/*****************************************************************************
 * Copyright (C) Stacy Curl. All rights reserved.                            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *****************************************************************************/
package picounit.features.mocking;

import org.jmock.builder.InvocationMockerBuilder;
import org.jmock.builder.InvocationMockerDescriber;
import org.jmock.builder.MatchBuilder;
import org.jmock.builder.NameMatchBuilder;
import org.jmock.cglib.Mock;
import org.jmock.core.Constraint;
import org.jmock.core.DynamicMock;
import org.jmock.core.Invocation;
import org.jmock.core.InvocationMatcher;
import org.jmock.core.InvocationMocker;
import org.jmock.core.Invokable;
import org.jmock.core.Stub;
import org.jmock.core.matcher.InvokeOnceMatcher;

import picounit.features.base.ProxyFactory;
import picounit.features.constraint_store.ConstraintStore;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import junit.framework.AssertionFailedError;

public class RecordingPlaybackMock<T> implements DynamicMock, InvocationHandler {
	private static final MethodUtil methodUtil = new MethodUtil();

	private final EmptyCGLIBCoreMock playbackCoreMock;
	private final RecordingPlaybackMockListener invocationListener;
	private final Mock playbackMock;
	private final T proxy;

	private final ConstraintStore constraintStore;
	private final MockInvocationObserver mockInvocationObserver;

	private boolean recording = true;
	private int expectationSequenceNumber = 0;
	private boolean breakInExpectations;
	private Stub stub;
	private InvocationMatcher invocationMatcher;
	private Invocation invocation;

	private String reason;

	private ReturnTypeModifier returnTypeModifier;

	private TypeFactory typeFactory;

	public RecordingPlaybackMock(Class<T> mockedType, String roleName,
		RecordingPlaybackMockListener invocationListener, MockInvocationObserver mockInvocationObserver,
		ProxyFactory proxyFactory, ReturnTypeModifier returnTypeModifier, ConstraintStore constraintStore,
		TypeFactory typeFactory) {

		this.returnTypeModifier = returnTypeModifier;
		this.constraintStore = constraintStore;
		this.mockInvocationObserver = mockInvocationObserver;
		this.invocationListener = invocationListener;
		this.playbackCoreMock = new EmptyCGLIBCoreMock(mockedType, roleName);
		this.playbackMock = new Mock(playbackCoreMock);
		this.proxy = proxyFactory.create(mockedType, this);
		
		this.typeFactory = typeFactory;
	}
	
	public void setStub(Stub stub) {
		this.stub = stub;
	}

	public void setInvocationMatcher(InvocationMatcher invocationMatcher) {
		this.invocationMatcher = invocationMatcher;
	}

	public void setReason(String reason) {
		this.reason = reason;
	}

	public void replay() {
		completeExpectation();
		recording = false;
	}

	public void record() {
		recording = true;
	}

	public boolean expectationComplete() {
		return invocation == null;
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Verifiable
	///////////////////////////////////////////////////////////////////////////////////////////////

	public void verify() {
		playbackCoreMock.verify();
		breakInExpectations = true;
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// DynamicMock
	///////////////////////////////////////////////////////////////////////////////////////////////

	public void addInvokable(Invokable invokable) {
		playbackCoreMock.addInvokable(invokable);
	}

	public Class getMockedType() {
		return playbackCoreMock.getMockedType();
	}

	public T proxy() {
		return proxy;
	}

	public void setDefaultStub(Stub newDefaultStub) {
		playbackCoreMock.setDefaultStub(newDefaultStub);
	}

	public void reset() {
		recording = true;
		playbackCoreMock.reset();
		invocationMatcher = null;
		stub = null;
		invocation = null;
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Invocation Handler
	///////////////////////////////////////////////////////////////////////////////////////////////

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		mockInvocationObserver.fire(method.getReturnType());

		if (methodUtil.isEquals(method)) {
			return proxy == args[0] ? Boolean.TRUE : Boolean.FALSE;
		}
		
		if (methodUtil.isToString(method)) {
			return playbackCoreMock.getMockName();
		}

		if (recording && !isDefault(method)) {
			completeExpectation();
			this.invocation = new Invocation(proxy, method, args);

			invocationListener.recordingPlaybackMockEvent(this);

			return defaultValue(method.getReturnType(), method.getDeclaringClass());
		}
		else {
			try {
				return playbackCoreMock.mockInvocation(method, args);
			}
			catch (AssertionFailedError assertionFailedError) {
				throw assertionFailedError;
			}
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Implementation
	///////////////////////////////////////////////////////////////////////////////////////////////

	// TODO: Return chained mocks
	@SuppressWarnings("unchecked")
	private <E> E defaultValue(Class<E> returnType, Class declaringClass)
		throws ClassNotFoundException {
		
		if (typeFactory.isNotCastable(returnType)) {
			return typeFactory.createNotCastable(returnType);
		}

		return typeFactory.create(returnTypeModifier.refine(returnType, declaringClass));
	}

	/*
	 * It would be better to access the default matchers registered in the
	 * AbstractDynamicMock.setupDefaultBehaviour
	 */
	private boolean isDefault(Method invokedMethod) {
		return methodUtil.isEquals(invokedMethod) ||
			methodUtil.isHashCode(invokedMethod) ||
			methodUtil.isToString(invokedMethod);
	}
	
	private void completeExpectation() {
		if (expectationComplete()) {
			return;
		}

		Class returnType = invocation.invokedMethod.getReturnType();

		if (isPrimative(returnType) && stub == null) {
			throw new AssertionFailedError(
				"a return value must be specified for methods returning primatives");
		}

		MatchBuilder matchBuilder = expects(getInvocationMatcher()).method(
			invocation.invokedMethod.getName()).with(
			getConstraints(invocation.parameterValues));

		addSequenceConstraint(matchBuilder);

		if (returnType.equals(Void.TYPE)) {
			matchBuilder.isVoid();
		}
		else if (stub != null) {
			matchBuilder.will(stub);
		}

		invocationMatcher = null;
		stub = null;
		invocation = null;
		reason = null;
	}

	private Constraint[] getConstraints(List parameterValues) {
		return constraintStore.getConstraints(getEqualsConstraints(parameterValues), parameterValues);
	}

	private Constraint[] getEqualsConstraints(List parameterValues) {
		List<Constraint> constraints = new ArrayList<Constraint>(parameterValues.size());
		
		for (Object parameter : parameterValues) {
			constraints.add(new ProxyAwareIsEqual(parameter));
		}
		
		return constraints.toArray(new Constraint[constraints.size()]);
	}

	private NameMatchBuilder expects(InvocationMatcher invocationMatcher) {
		InvocationMocker mocker = new InvocationMocker(getDescriber());

		playbackMock.addInvokable(mocker);

		NameMatchBuilder builder = getBuilder(mocker);

		builder.match(invocationMatcher);

		return builder;
	}

	private InvocationMatcher getInvocationMatcher() {
		return invocationMatcher != null ? invocationMatcher : new InvokeOnceMatcher();
	}

	private InvocationMockerDescriber getDescriber() {
		return reason == null
			? new InvocationMockerDescriber()
			: new InvocationMockerDescriberWithReason(reason);
	}

	private InvocationMockerBuilder getBuilder(InvocationMocker invocationMocker) {
		return new InvocationMockerBuilder(invocationMocker, playbackMock, getMockedType());
	}

	private boolean isPrimative(Class returnType) {
		return returnType.isPrimitive() && !returnType.equals(Void.TYPE);
	}

	// TODO: Only add sequence constraint when two invocations of the same method with the same
	//		parameters follow each other. This will be sufficient to recover the ordering lost by jMock.
	private void addSequenceConstraint(MatchBuilder matchBuilder) {
		matchBuilder.id("" + expectationSequenceNumber);
		if (expectationSequenceNumber > 0 && !breakInExpectations) {
			matchBuilder.after("" + (expectationSequenceNumber - 1));
		}

		breakInExpectations = false;
		expectationSequenceNumber++ ;
	}
}
